Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Priming the cache #168

Merged
merged 1 commit into from
Feb 23, 2024
Merged

Priming the cache #168

merged 1 commit into from
Feb 23, 2024

Conversation

tgwizard
Copy link
Contributor

@tgwizard tgwizard commented Feb 22, 2024

Sometimes data is loaded through some means, which will also later be loaded by an independent batch data loader. It can be convenient to then prime the cache of that data data loader with the already loaded data, to remove redundant calls.

The use-case I have can be summarized by this query:

query {
  shopsFollowed(first: 10) {
    nodes {
      id
      followedByMe
    }
  }
}

The shopsFollowed resolver performs a query to load 10 shops that the current user follows. This is returned as the Shop type, which is used extensively in the API. The Shop.followedByMe field returns a boolean of whether the current user follows the shop. It's backed by a custom batch data loader.

For the specific shopsFollowed use-case, we know that all the shops returned are followed by the current user, so there's no need for the custom batch data loader to load this again.

With prime we can have the shopsFollowed resolver pre-populate this information on the loader.

This is available in other graphql batch data loader libraries, see e.g.

@tgwizard tgwizard requested review from swalkinshaw and a team February 22, 2024 13:28
Copy link
Contributor

@swalkinshaw swalkinshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice and simple 🎉

Copy link

@michaelherold michaelherold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how simple this is. I wonder if the simplicity could potentially hide performance bugs. What do you think?

Comment on lines +235 to +253
def test_prime_will_not_replace_already_cached_value
loader = EchoLoader.for

assert_equal :a, loader.load(:a).sync

loader.prime(:a, :not_a)

assert_equal :a, loader.load(:a).sync
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: This seems like it potentially is a footgun for surprising behavior. Particularly with ActiveRecord's lazy loading of associations, what if you try to prime with a model that has preloaded an association that you need (N.B. if you're using GraphQL::Batch, you probably shouldn't be loading associations like this, but stranger things have happened), but it has already been fulfilled with a version that does not have that association preloaded?

question: Should we emit a warning or something by default when attempting to prime an already-fulfilled promise? One could then opt in to allow either overriding or ignoring the warning.

This could very well be overkill but it's the first thing that came to mind when I saw the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is indeed not meant to be used to populate the loader with values that are in a certain state, for some kind of perf reasons. The values you put in should behave the same as the values the loader would have loaded on its own.

I'm not sure there's a good and simple way to protect against this, except in documenting its intended use.

Given this, I don't think we should emit a warning when priming a value for a key that already exists in the loader cache. That is meant to be a no-op - loading or priming should result in equivalent results - so it shouldn't seem to the developer that they are doing something wrong. Given the resolution of GraphQL it can be very hard to reason or know/ensure that you'd be priming keys that won't exist.

The behaviour here (priming inserts a key in the cache only if the key didn't exist) is also the behaviour of the other data loader libraries I found.

What do you think?

@tgwizard
Copy link
Contributor Author

I updated the prime logic a bit, to deal with the cases when your prime a key that's already in the queue:

     def prime(key, value)
-      promise = cache[cache_key(key)] ||= ::Promise.new.tap { |p| p.source = self }
-      promise.fulfill(value) unless promise.fulfilled?
+      cache[cache_key(key)] ||= ::Promise.resolve(value).tap { |p| p.source = self }
     end

@tgwizard tgwizard merged commit 7aaf3a2 into main Feb 23, 2024
23 checks passed
@tgwizard tgwizard deleted the prime branch February 23, 2024 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants